package com.talis.platform.client;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.log4j.Logger;
import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QueryFactory;
import com.hp.hpl.jena.query.QueryParseException;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.query.ResultSetFactory;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.sparql.resultset.ResultSetFormat;
import com.talis.http.HttpHeaders;
import com.talis.http.MediaTypes;
import com.talis.http.Response;
import com.talis.platform.client.jaxb.*;

public class SparqlService {

    private static final Logger log = Logger.getLogger(SparqlService.class);
    
    private String myUri;
    private Credentials myCredentials;
    
    public SparqlService(String uri){
        myUri = uri;
    }
    
    public SparqlService(String uri, Credentials credentials){
        myUri = uri;
        myCredentials = credentials;
    }
    
    public String getUri(){
        return myUri;
    }
    
    public Credentials getCredentials(){
        return myCredentials;
    }
    
    public Model graph(String query) throws HttpException, IOException, 
                                            ParseException, InvalidQueryFormException,
                                            ClientException, ServiceException{
        try{
            Query q = QueryFactory.create(query);
            if (! q.isConstructType() && ! q.isDescribeType()){
                String form = q.isAskType() ? "ASK" : q.isSelectType() ? "SELECT" : "UNKNOWN"; 
                throw new InvalidQueryFormException(String.format("Query form %s does not answer a Graph", form));
            }
        }catch(QueryParseException qpe){
            throw new ParseException("Could not parse supplied query", qpe);
        }
        
        PostMethod post = postQueryToService(query, MediaTypes.RDFXML);
        if (post.getStatusCode() == 200){
            return parseResponseBodyAsModel(post);
        }else if (post.getStatusCode() >= 400 && post.getStatusCode() <= 499){
            throw new ClientException("The service returned a client side error response", 
                    buildResponse(post));
        }else if (post.getStatusCode() >= 500 && post.getStatusCode() <= 599){
            throw  new ServiceException("The service returned a service side error response", 
                    buildResponse(post));
        }else{
            throw new UndeterminedResultException("Unable to determine result from server response", 
                    buildResponse(post));
        }
    }
    
    public boolean ask(String query) throws HttpException, IOException, 
                                            ParseException, InvalidQueryFormException,
                                            ClientException, ServiceException{

        try{
            Query q = QueryFactory.create(query);
            if (! q.isAskType()){
                throw new InvalidQueryFormException("Query was not of ASK form");
            }
        }catch(QueryParseException qpe){
            throw new ParseException("Could not parse supplied query", qpe);
        }

        PostMethod post = postQueryToService(query, MediaTypes.SPARQL_RESULTS);
        if (post.getStatusCode() == 200){
            try{
                return parseResponseBodyAsBoolean(post);
            }catch(Exception e){
                String message = "Unable to parse server response into ResultSet";
                log.error(message, e);
                throw new UndeterminedResultException(message, buildResponse(post), e);
            }
        }else if (post.getStatusCode() >= 400 && post.getStatusCode() <= 499){
            throw new ClientException("The service returned a client side error response", 
                    buildResponse(post));
        }else if (post.getStatusCode() >= 500 && post.getStatusCode() <= 599){
            throw  new ServiceException("The service returned a service side error response", 
                    buildResponse(post));
        }else{
            throw new UndeterminedResultException("Unable to determine result from server response", 
                    buildResponse(post));
        }
    }

    public ResultSet select(String query) throws HttpException, IOException, 
                                    ParseException, InvalidQueryFormException,
                                    ClientException, UndeterminedResultException,
                                    ServiceException{

        try{
            Query q = QueryFactory.create(query);
            if (! q.isSelectType()){
                throw new InvalidQueryFormException("Query was not of SELECT form");
            }
        }catch(QueryParseException qpe){
            throw new ParseException("Could not parse supplied query", qpe);
        }

        PostMethod post = postQueryToService(query, MediaTypes.SPARQL_RESULTS);
        if (post.getStatusCode() == 200){
            try{
                return ResultSetFactory.load(post.getResponseBodyAsStream(), ResultSetFormat.syntaxXML);
            }catch(Exception e){
                String message = "Unable to parse server response into ResultSet";
                log.error(message, e);
                throw new UndeterminedResultException(message, buildResponse(post), e);
            }
        }else if (post.getStatusCode() >= 400 && post.getStatusCode() <= 499){
            throw new ClientException("The service returned a client side error response", 
                    buildResponse(post));
        }else if (post.getStatusCode() >= 500 && post.getStatusCode() <= 599){
            throw  new ServiceException("The service returned a service side error response", 
                    buildResponse(post));
        }else{
            throw new UndeterminedResultException("Unable to determine result from server response", 
                    buildResponse(post));
        }
    }

    private PostMethod postQueryToService(String query, String mediaType) throws UnsupportedEncodingException, IOException, HttpException {
        if (log.isDebugEnabled()) { log.debug(String.format("POSTing query %s to %s", query, myUri)); }
        HttpClient client = new HttpClient();
        PostMethod post = new PostMethod(myUri);
        String entityBody = "query=" + URLEncoder.encode(query, "UTF-8");
        post.setRequestEntity(new StringRequestEntity(entityBody, MediaTypes.FORM_ENCODED, "UTF-8"));
        post.addRequestHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.FORM_ENCODED);
        post.addRequestHeader(HttpHeaders.ACCEPT, mediaType);
        client.executeMethod(post);
        if (log.isDebugEnabled()) { log.debug(String.format("POSTed query %s to %s, building response", query, myUri)); }
        return post;
    }
    
    private Model parseResponseBodyAsModel(HttpMethod method) throws UndeterminedResultException{
        Model model = ModelFactory.createDefaultModel();
        try{
            model.read(method.getResponseBodyAsStream(), "", "RDF/XML");
            return model;
        }catch(Exception e){
            String message = "Unable to parse response from service as RDF/XML";
            log.warn(message, e);
            throw new UndeterminedResultException(message,e);
        }
    }
    
    private boolean parseResponseBodyAsBoolean(HttpMethod method) throws UndeterminedResultException{
        try{
            return ObjectFactory.getSparqlBooleanResult(method.getResponseBodyAsStream());
        }catch(Exception e){
            String message = "Unable to parse response from service as boolean result";
            log.error(message, e);
            throw new UndeterminedResultException(message, e);
        }
    }
    
    private Response buildResponse(HttpMethod method) throws IOException{
        Response response = new Response(method.getStatusCode());
        for (Header header: method.getResponseHeaders()){
            response.addHeader(header.getName(), header.getValue());
        }
        response.setEntityBody(method.getResponseBodyAsStream());
        if (log.isDebugEnabled()) { log.debug("Built response"); }
        return response;
    }
    
       
    

    
}
